using GLib;
using Gee;

public class fileIndexDB  : Object{

	public signal void dataChanged();
	public signal void importFinished(string file, string[] symlink_targets);

	vSqlite.vDatabase db;
	public string dbfile;
	
	public fileIndexDB(string dbfile){
		this.dbfile = dbfile;
		try{
			File.new_for_path (dbfile + ".d").make_directory_with_parents ();
		}catch{}
		
		if (!File.new_for_path (dbfile).query_exists (null)){
			verboselog (@"initiate new fileIndexDB at >$dbfile<");
			db = new vSqlite.vDatabase(dbfile);
			db.exec ("CREATE TABLE env (dbname TEXT PRIMARY KEY)");
			db.exec ("CREATE TABLE watch (dir TEXT PRIMARY KEY, lastcheck TEXT)");
		} else {
			verboselog (@"create pre-access backup of fileIndexDB at >$dbfile<");
			File.new_for_path (dbfile).copy(File.new_for_path (dbfile + ".bak"), FileCopyFlags.OVERWRITE, null, null); 
			verboselog (@"open fileIndexDB at >$dbfile<");
			db = new vSqlite.vDatabase(dbfile);
		}
		
		foreach (string dir in getWatchDirectories ()){
			addWatchDir (dir);
		}
	}

	//=================WATCH DIRECTORIES MANAGEMENT

	
	public Gee.HashMap<string, DirectoryMonitor> mapWatchDirs = new Gee.HashMap<string, DirectoryMonitor>();
	void addWatchDir(string directory){
		verboselog (@"Watching directory $directory");

		DirectoryMonitor monitor = new DirectoryMonitor(File.new_for_path (directory));

		monitor.change.connect((file, otherfile, event)=>{
			string path = file.get_path();
			string otherpath = "";
			if (otherpath != null)
				otherpath = otherfile.get_path();
			
			if (event == FileMonitorEvent.CHANGED){
				foreach (string dbname in getAllDBnames ()){
					if (getValues (dbname, "std_path")[0] == file.get_path()){
						backupFile (dbname);
						return;
					}
				}
			}else if (event == FileMonitorEvent.CREATED){
				verboselog ("Watcher: created " + file.get_path());
				foreach (string dbname in getAllDBnames ()){//Backup and abort if file is already indexed
					if (getValues (dbname, "std_path")[0] == file.get_path()){
						backupFile (dbname);
						return;
					}
				}
				import (file);
			}else if (event == FileMonitorEvent.DELETED){
				verboselog ("Watcher: deleted " + file.get_path());
				removeFile (file);
			}else if (event == FileMonitorEvent.MOVED){
				verboselog ("Watcher: moved " + file.get_path() + " to " + otherfile.get_path());
				foreach (string dbname in getAllDBnames ()){
					string cfile = getValues (dbname, "std_path")[0];
					
					if (cfile.has_prefix(path)){
						setValues (dbname, "std_path", new string[1]{ cfile.replace(path, otherpath) });
						backupFile (dbname);
					}
				}
			}
		});
		
		mapWatchDirs.set(directory, monitor);
	}
	void removeWatchDir(string directory){
		mapWatchDirs[directory].destroy();
		mapWatchDirs.unset (directory);
	}
	
	public string[] getWatchDirectories(){
		return db.exec ("SELECT dir FROM watch");
	}
	public void removeWatchDirectory(string directory){
		if (directory in getWatchDirectories ()){
			db.exec (@"DELETE FROM watch WHERE dir='$directory'");
			mapWatchDirs[directory].destroy();
			removeWatchDir (directory);
			removeFile (File.new_for_path (directory));
		}
	}

	public bool addWatchDirectory(string directory){
		//Abort if the new directory and an already existing directory are related
		if (conflictsWithWatchDirectory (directory))
			return false;
		
		//If given directory is a symlink, resolve it completely before importing
		string target = directory;
		FileType type = File.new_for_path(target).query_file_type (FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
		while (type == FileType.SYMBOLIC_LINK){
			target = File.new_for_path(target).query_info(FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null).get_symlink_target();
			type = File.new_for_path(target).query_file_type (FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
		}
		
		DateTime now = new DateTime.now_local ();
		db.exec (@"INSERT INTO watch VALUES('$target', " + now.to_unix ().to_string () + ")");

		unowned Thread thd = Thread.create<void*> (()=>{
			addWatchDir (target);
			return null;
		}, true);
		import (File.new_for_path (target), directory);
		return true;
	}

	bool conflictsWithWatchDirectory(string directory){
		string[] wdirs = getWatchDirectories ();
		foreach (string wdir in wdirs){
			if (wdir == directory)
				return true;
			if (wdir.split("/").length == directory.split("/").length)//Continue if paths have the same depth but not the same name -> different
				continue;
			
			if (wdir.has_prefix(directory) | directory.has_prefix (wdir))//Check if directories are related
				return true;
		}
		return false;
	}

	//=================FILE MANAGEMENT
	
	public string[] getAllDBnames(){
		return db.exec("SELECT dbname FROM env");
	}

	public string[] getValues(string fileDBname, string property){
		try{
			string[] ret = db.exec(@"SELECT * FROM '$fileDBname' WHERE property='$(sqlverbatim (property))'");
			if (ret.length < 2)
				return new string[0];
			return ret[1].split("||");//Pars possibly multiple values
		}catch{ return new string[0]; }
	}
	
	public string[] getProperties(string fileDBname){
		try{
			return db.exec(@"SELECT property FROM '$fileDBname'");
		}catch{ return new string[0]; }
	}
	
	void setValues(string fileDBname, string property, string[] values){
		try{
			//Replace existing property with new array
			removeProperty(new string[]{fileDBname}, property);
			db.exec(@"INSERT INTO '$fileDBname' VALUES ('$(sqlverbatim (property))', '" + _serializeArray(values, true) + "')");
		}catch{ return; }
	}
	public void removeProperty(string[] fileDBnames, string property){
		try{
			//db.exec("BEGIN TRANSACTION;");
			foreach (string fileDBname in fileDBnames){
				if (!fileIndexed(fileDBname))
					continue;
				db.exec(@"DELETE FROM '$fileDBname' WHERE property='$(sqlverbatim (property))'");
			}
			//db.exec("END TRANSACTION;");
		}catch{ return; }
	}
	public bool addValue(string[] fileDBnames, string property, string val){//For addValue and removeValue no fileIndexed check is necessary (already in getValues etc)
		if ("||" in val)//Abort if val contains serialization string
			return false;
		
		//db.exec("BEGIN TRANSACTION;");
		foreach (string fileDBname in fileDBnames){
			string[] v = getValues(fileDBname, property);
			if (val in v)//Abort if value already exists
				continue;
			v += val;
			setValues (fileDBname, property, v);
		}
		//db.exec("END TRANSACTION;");
		return true;
	}
	public void removeValue(string[] fileDBnames, string property, string val){
		//db.exec("BEGIN TRANSACTION;");
		foreach (string fileDBname in fileDBnames){
			string[] newVals = new string[0];
			foreach (string v in getValues(fileDBname, property)){
				if (v != val)
					newVals += v;
			}
			setValues (fileDBname, property, newVals);
		}
		//db.exec("END TRANSACTION;");
	}
	bool fileIndexed(string dbname){
		if (db.exec ("SELECT name FROM sqlite_master WHERE type='table' AND name='" + dbname + "'").length == 0)
			return false;
		return true;
	}

	//Remove existing file
	void removeFile(File file){
		while (toBeRemoved != null)
			Thread.usleep (10000);
		toBeRemoved = file;
		unowned Thread thd = Thread.create<void*> (()=>{
			
			string removePath = toBeRemoved.get_path();
			toBeRemoved = null;

			while (threadLock)
				Thread.usleep (100 * 1000);
			threadLock = true;
			
			db.exec("BEGIN TRANSACTION;");
			foreach (string dbname in getAllDBnames ())
				if (getValues (dbname, "std_path")[0].has_prefix(removePath)){
					verboselog (@"Remove file $dbname");
					db.exec(@"DELETE FROM 'env' WHERE dbname='$dbname'");
					db.exec (@"DROP TABLE '$dbname'");
					removeBackups (dbname);
				}
			db.exec("END TRANSACTION;");
			
			threadLock = false;
			
			dataChanged ();
			return null;
		}, true);
	}
	bool removeThreadLock = false;
	GLib.File? toBeRemoved = null;//Used to forward file to new thread


	//Import new file

	public void import(GLib.File file, string reportBackPath = ""){
		while (toBeImported != null)
			Thread.usleep (10000);
		toBeImported = file;
		toBeReported = reportBackPath;
		
		unowned Thread thd = Thread.create<void*> (()=>{
			File clone = File.new_for_path (toBeImported.get_path());
			string reportPathClone = toBeReported;
			toBeImported = null;

			while (threadLock){
				Thread.usleep (100 * 1000);
			}
			threadLock = true;

			newDbname = 0;
			dbnameRegister = db.exec (@"SELECT * FROM env");
			fileImportEndTransactionCounter = 0;

			db.exec("BEGIN TRANSACTION;");
			string[] foundLinks = import_backstage (clone);
			db.exec("END TRANSACTION;");
			verboselog ("import finished");
			threadLock = false;

			//Output returned link targets
			string[] returnedLinks = new string[0];
			foreach (string link in foundLinks){
				if (!conflictsWithWatchDirectory (link))
					returnedLinks += link;
			}

			if (reportPathClone == "")
				importFinished(clone.get_path(), returnedLinks);
			else
				importFinished(reportPathClone, returnedLinks);
			dataChanged ();
			return null;
		}, true);
	}
	GLib.File? toBeImported = null;//Used to forward import target to spawned thread
	string toBeReported = "";
	
	string[] dbnameRegister;
	int newDbname = 0;

	int fileImportEndTransactionCounter = 0;
	bool threadLock = false;
	string[] import_backstage(File file){
		
		if (!_fileIsVisible (file))//Ignore invisible files
			return new string[0];
		FileType type = file.query_file_type (FileQueryInfoFlags.NOFOLLOW_SYMLINKS);
		
		if (type == FileType.SYMBOLIC_LINK){//If symlink, return symlink target
			string target = file.query_info(FILE_ATTRIBUTE_STANDARD_SYMLINK_TARGET, FileQueryInfoFlags.NOFOLLOW_SYMLINKS, null).get_symlink_target();
			return new string[]{target};
		}
		
		if (type == FileType.DIRECTORY){//If given file is a directory, import all children
			FileEnumerator enumer;
			try{//Abort if permissions do not suffice
				enumer = file.enumerate_children (FILE_ATTRIBUTE_STANDARD_NAME, 0);
			}catch{
				return new string[0];
			}
			
			FileInfo file_info;
			string[] collectedLinks = new string[0];
			
			while (true) {
				file_info = enumer.next_file ();
				if (file_info == null)
					break;
				
				string[] imp = import_backstage(File.new_for_path (file.get_parse_name () + "/" + file_info.get_name ()));
				foreach (string i in imp)//Collect returned symlink targets
					if (!(i in collectedLinks))
						collectedLinks += i;
			}
			return collectedLinks;
		}


		//Actual file import
		
		verboselog("Importing >" + file.get_path() + "<");
		
		int i = file.get_basename ().last_index_of (".", 0);//get extension / display name
		string name = file.get_basename ().substring (0, i);
		string ext = "";//Determine extension only if there is a "." in the basename
		if (i > 0)
			ext = file.get_basename ().substring (i + 1, file.get_basename ().length - i - 1).casefold();

		string tblName = "";

		//Look for free dbname in table env

		while (tblName == ""){
			if (!(newDbname.to_string() in dbnameRegister)){
				tblName = newDbname.to_string();
			}
			newDbname ++;
		}
		dbnameRegister += tblName;

		//Register new dbname in env table
		db.exec("INSERT INTO env VALUES ('" + tblName + "');");
		db.exec("CREATE TABLE '" + tblName + "' (property TEXT PRIMARY KEY, value TEXT);");

		//Then create individual table and add standard values. (All in one exec to improve performance!)

		db.exec("INSERT INTO '" + tblName + "' VALUES ('std_dbname', '" + tblName + "');	");
		db.exec("INSERT INTO '" + tblName + "' VALUES ('std_path', '" + sqlverbatim(file.get_path()) + "');");
		db.exec("INSERT INTO '" + tblName + "' VALUES ('std_displayname', '" + sqlverbatim(name) + "');");
		db.exec("INSERT INTO '" + tblName + "' VALUES ('std_extension', '" + sqlverbatim(ext) + "');");

		fileImportEndTransactionCounter++;//Avoid dbus connection timeout by allowing database access for a short while
		if (fileImportEndTransactionCounter > 20){
			fileImportEndTransactionCounter = 0;
			db.exec("END TRANSACTION;");
			Thread.usleep(1000);
			db.exec("BEGIN TRANSACTION;");
		}
		
		return new string[0];
	}


	//=========BACKUP MANAGEMENT

	void backupFile(string dbname){
		int numBackups = getNumberOfBackups (dbname);
		if (numBackups == -1){
			verboselog (@"not backing up file $dbname");
			return;
		}
		
		string path = getValues (dbname, "std_path")[0];
		verboselog (@"backing up file $dbname >$path<");

		
		File file = File.new_for_path (path);//Create backup
		FileInfo inf = file.query_info (GLib.FILE_ATTRIBUTE_TIME_MODIFIED, FileQueryInfoFlags.NONE, null);

#if VALA_MINOR_0_16
		TimeVal modtime;
		inf.get_modification_time (out modtime);
#else
		TimeVal modtime = inf.get_modification_time ();
#endif

		string currentTime = modtime.to_iso8601 ();
		
		string latestTime = "";
		if (numBackups > 0){
			File latestBackup = File.new_for_path (dbfile + ".d/" + dbname + "_" + (numBackups - 1).to_string ());//Create backup
			FileInfo latestBackupInf = latestBackup.query_info (GLib.FILE_ATTRIBUTE_TIME_MODIFIED, FileQueryInfoFlags.NONE, null);
#if VALA_MINOR_0_16
			TimeVal latestmodtime;
			latestBackupInf.get_modification_time (out latestmodtime);
			latestTime = latestmodtime.to_iso8601 ();
#else
			latestTime = latestBackupInf.get_modification_time ().to_iso8601 ();
#endif
		}
		
		if (currentTime != latestTime){
			db.exec ("DELETE FROM '" + dbname + "_bak' WHERE number=-1");//Update backup counter
			db.exec("INSERT INTO '" + dbname + "_bak' VALUES ('-1', '" + (numBackups + 1).to_string() + "');");
			db.exec("INSERT INTO '" + dbname + "_bak' VALUES ('" + numBackups.to_string() + "', '" + currentTime + "');");
			
			file.copy (File.new_for_path (dbfile + ".d/" + dbname + "_" + numBackups.to_string ()), FileCopyFlags.OVERWRITE, null, null);
		}
	}
	public int getNumberOfBackups(string dbname){
		if (db.exec ("SELECT name FROM sqlite_master WHERE type='table' AND name='" + dbname + "_bak'").length == 0)
			return -1;
		string sqlret = db.exec (@"SELECT date FROM '" + dbname + "_bak' WHERE number=-1")[0];
		return int.parse(sqlret);
	}
	public string getBackupPath(string dbname, int backupID){
		return dbfile + ".d/" + dbname + "_" + backupID.to_string ();
	}
	void removeBackups(string dbname){
		int numBackups = getNumberOfBackups (dbname);
		if (numBackups > -1){
			for (int q = 0; q < numBackups; q++)
				File.new_for_path (getBackupPath (dbname, q)).delete();
			
			db.exec("DROP TABLE '" + dbname + "_bak'");
		}
	}
	public void setBackupState(string dbname, bool backup){
		if (backup){
			if (getNumberOfBackups (dbname) == -1){
				db.exec("CREATE TABLE '" + dbname + "_bak' (number INTEGER PRIMARY KEY, date TEXT);");
				db.exec("INSERT INTO '" + dbname + "_bak' VALUES ('-1', '0');");
				backupFile (dbname);
			}
		}else{
			removeBackups(dbname);
		}
	}
	public string getBackupDate(string dbname, int backupID){
		return db.exec (@"SELECT date FROM '" + dbname + "_bak' WHERE number=" + backupID.to_string())[0];
	}
	
	//Helper functions

	static string sqlverbatim(string str){
		return str.replace("'", "''");
	}

	static string _serializeArray(string[] array, bool verbatim){
		string ret = "";
		for (int q = 0; q < array.length; q++){
			if (verbatim)
				ret += sqlverbatim (array[q]);
			else
				ret += array[q];
			if (q < array.length - 1)
				ret += "||";
		}
		return ret;
	}

}
